Option Explicit

'################################################################
' Discriptor
' 二维数据对象
'################################################################

Private Const RANDOM_ID_SIZE As Integer = 15

Implements IDataFrame

'################################################################
' Field
'################################################################

' 列集合
' 使用列式存储
' 使用数组保存各个列
' 发生删除操作时，需要重新设置
' List<ColIndex, Series> --> Series[]
Private fieldColumns() As IDFSeries

' 列索引-->列名的映射
' 使用数组保存映射
' 发生删除操作时，需要重新设置
Private fieldColumnNames() As String

' 列名-->列索引的映射
' 发生删除操作时，需要重新设置
' Map<colName, colIndex>
Private columnNameIndexMap As Object

' 当前 dataFrame 的Id
Private fieldId As String

' 行数
Private fieldSize As Long

' 列数
Private fieldColCount As Long

' 保存数据的描述器
' Map<index, IDFDescriptor>
Private dataDescriptors As Object

'################################################################
' Constructor
'################################################################
Private Sub Class_Initialize()
End Sub

Private Sub Class_Terminate()
End Sub

'################################################################
' Init
'################################################################
' @otional-param colNames String[] 列名数组（如果提供了列名数组将会忽略 colCount 列名数量 参数的值）
' @otional-param colCount 列名数量
' @otional-param id 当前 DataFrame 的 ID，如果没有提供，将会随机生成一个 ID
' @otional-param dTypes DF_DataType[]
'                       各列的类型，如果没有提供，则所有列全部使用 String 类型
'                       如果提供了 dTypes，则数量必须和列数相等
' @otional-param initRowSize 初始化的行缓存数量，默认=10
Public Sub Init( _
    Optional ByRef colNames As Variant, _
    Optional ByRef colCount As Integer, _
    Optional ByRef id As String = "", _
    Optional ByRef dTypes As Variant, _
    Optional ByRef initRowSize As Long = 10 _
)
    ' 1. 确定列数 + 列名
    Dim cnMin As Integer
    Dim hasColName As Boolean

    ' 1.1 尝试使用列名数组来确定列数
    If IsArray(colNames) = True Then
        cnMin = LBound(colNames)

        ' 计算列的数量
        fieldColCount = UBound(colNames) - cnMin + 1

        ' 设置标记，当前参数中存在列名
        hasColName = True
    End If

    ' 1.2 如果没有传递列名数组，或者传递了一个空的列名数组，则尝试从 colCount 获取列数
    If fieldColCount = 0 Then
        If colCount <= 0 Then
            Err.Raise 10211, Description:="DataFrame.Init: please set parameter: colNames or colCount"
            Exit Sub
        End If

        fieldColCount = colCount
    End If

    ' 2. 检查 dTypes 的数量 和 列数是否相等
    Dim i As Integer

    Dim realDTypes() As DF_DataType
    ReDim realDTypes(0 To (fieldColCount - 1))
    Dim dTypesMin As Integer
    Dim dTypesMax As Integer
    Dim dTypesCount As Integer
    ' 确定 dTypes 的数量
    If IsArray(dTypes) = True Then
        dTypesMin = LBound(dTypes)
        dTypesMax = UBound(dTypes)
        dTypesCount = dTypesMax - dTypesMin + 1
    End If

    If dTypesCount = 0 Then
        ' 如果 dTypes 的数量为 0，说明没有传递参数 dTypes，或者 dTypes 是一个空数组
        ' 则按照列数，创建全 String 类型
        For i = 0 To (fieldColCount - 1)
            realDTypes(i) = DF_DataType.DF_STRING
        Next
    Else
        ' 如果传递了有效的 dTypes
        ' 确认 dTypes 的数量 和 列数是否相等
        If dTypesCount <> fieldColCount Then
            ' 如果 dTypes 数量 != 列数，则立刻终止，并抛出异常
            Err.Raise 10212, Description:="DataFrame.Init: count of parameter [dTypes] is not equals count of column. dTypesCount=" & dTypesCount & ", columnCount=" & fieldColCount
            Exit Sub
        End If

        ' 拷贝一份 dTypes，以保证下标从 0 开始
        For i = 0 To (fieldColCount - 1)
            realDTypes(i) = dTypes(dTypesMin + i)
        Next
    End If

    ' 3. 生成列索引、列名的映射关系
    ReDim fieldColumnNames(0 To (fieldColCount - 1))
    Set columnNameIndexMap = UtilsCreateSysObj.createMap
    If hasColName = True Then
        ' 如果有参数中有列名，则直接使用
        For i = 0 To (fieldColCount - 1)
            fieldColumnNames(i) = colNames(cnMin + i) & ""

            ' 如果某个列名已经存在，则抛出异常
            If columnNameIndexMap.Exists(fieldColumnNames(i)) Then
                Err.Raise 10223, Description:="DataFrame.Init: DataFrame.Init: ColName[" & fieldColumnNames(i) & "] is repeat in parameter colNames(" & (cnMin + i) & ")"
                Exit Sub
            End If

            columnNameIndexMap.Add fieldColumnNames(i), i
        Next
    Else
        ' 如果参数中没有列名，则使用索引作为列名
        For i = 0 To (fieldColCount - 1)
            fieldColumnNames(i) = "" & i

            columnNameIndexMap.Add fieldColumnNames(i), i
        Next
    End If

    ' 4. 生成列对象
    ReDim fieldColumns(0 To (fieldColCount - 1))
    Dim s As DFSeriesInner
    For i = 0 To (fieldColCount - 1)
        ' 生成列对象
        Set s = New DFSeriesInner
        s.Init dType:=realDTypes(i), size:= initRowSize

        ' 缓存
        Set fieldColumns(i) = s
    Next

    ' 5. 保存 ID
    fieldId = Trim(id)
    If fieldId = "" Then
        fieldId = UtilsRandom.enNumStr(RANDOM_ID_SIZE)
    End If
End Sub

' 使用指定的 IDFSeries 数组作为列对象来初始化当前 DataFrame
' @param colArray 列数组 IDFSeries[]
' @param nameArray 列名数组 String[]
Public Sub InitBySeries(ByRef colArray As Variant, ByRef nameArray As Variant)
    ' 1. 检查参数
    ' 1.1 两个参数必须是数组
    If Not IsArray(colArray) Then
        Err.Raise 10005, Description:="DataFrame.InitBySeries: parameter colArray must be Array"
        Exit Sub
    End If
    If Not IsArray(nameArray) Then
        Err.Raise 10005, Description:="DataFrame.InitBySeries: parameter nameArray must be Array"
        Exit Sub
    End If

    ' 1.2 两个数组的长度不能是 0
    Dim colArrayIdxMin As Integer
    Dim colArrayIdxMax As Integer
    Dim colArraySize As Integer
    colArrayIdxMin = LBound(colArray)
    colArrayIdxMax = UBound(colArray)
    colArraySize = colArrayIdxMax - colArrayIdxMin + 1

    If colArraySize = 0 Then
        Err.Raise 10006, Description:="DataFrame.InitBySeries: Array parameter colArray's size must not be 0"
        Exit Sub
    End If

    Dim nameArrayIdxMin As Integer
    Dim nameArrayIdxMax As Integer
    Dim nameArraySize As Integer
    nameArrayIdxMin = LBound(nameArray)
    nameArrayIdxMax = UBound(nameArray)
    nameArraySize = nameArrayIdxMax - nameArrayIdxMin + 1

    If nameArraySize = 0 Then
        Err.Raise 10006, Description:="DataFrame.InitBySeries: Array parameter nameArray's size must not be 0"
        Exit Sub
    End If

    ' 1.3 两个数组的长度必须相同
    If colArraySize <> nameArraySize Then
        Err.Raise 10221, Description:="DataFrame.InitBySeries: array parameter [colArray] and [nameArray] 's size is not same. colArray.size=" & colArraySize & ", nameArray.size=" & nameArraySize
        Exit Sub
    End If

    ' 1.4 检查 colArray 的所有类型必须是 IDFSeries
    Dim v As Variant
    Dim colIdx As Integer
    colIdx = 0
    For Each v In colArray
        If Not IsObject(v) Then
            Err.Raise 10222, Description:="DataFrame.InitBySeries: parameter colArray(" & (colArrayIdxMin + colIdx) & ")'s type must be IDFSeries"
            Exit Sub
        End If
        If v Is Nothing Then
            Err.Raise 10001, Description:="DataFrame.InitBySeries: parameter colArray(" & (colArrayIdxMin + colIdx) & ") must be not nothing"
            Exit Sub
        End If
        If Not TypeOf v Is IDFSeries Then
            Err.Raise 10222, Description:="DataFrame.InitBySeries: parameter colArray(" & (colArrayIdxMin + colIdx) & ")'s type must be IDFSeries"
            Exit Sub
        End If

        colIdx = colIdx + 1
    Next

    ' 1.5 检查 nameArray 的所有类型必须是 String
    colIdx = 0
    For Each v In nameArray
        If IsObject(v) Then
            Err.Raise 10222, Description:="DataFrame.InitBySeries: parameter nameArray(" & (nameArrayIdxMin + colIdx) & ")'s type must be String"
            Exit Sub
        End If

        colIdx = colIdx + 1
    Next

    ' 2. 生成 【列索引-->列名的映射】 + 【列名-->列索引的映射】
    Set columnNameIndexMap = UtilsCreateSysObj.createMap
    ReDim fieldColumnNames(0 To (nameArraySize - 1))
    For colIdx = 0 To (nameArraySize - 1)
        ' 列索引-->列名的映射
        fieldColumnNames(colIdx) = nameArray(nameArrayIdxMin + colIdx) & ""

        ' 列名-->列索引的映射
        ' 如果某个列名已经存在，则抛出异常
        If columnNameIndexMap.Exists(fieldColumnNames(colIdx)) Then
            Err.Raise 10223, Description:="DataFrame.InitBySeries: ColName[" & fieldColumnNames(colIdx) & "] is repeat in parameter nameArray(" & (nameArrayIdxMin + colIdx) & ")"
            Exit Sub
        End If

        columnNameIndexMap.Add fieldColumnNames(colIdx), colIdx
    Next

    ' 3. 生成列集合
    '    确认行数，以 colArray 中的所有 IDFSeries.Size 的最小值为准
    ReDim fieldColumns(0 To (colArraySize - 1))
    fieldSize = -1

    Dim series As IDFSeries
    For colIdx = 0 To (colArraySize - 1)
        ' 保存列
        Set series = colArray(colArrayIdxMin + colIdx)
        Set fieldColumns(colIdx) = series

        ' 确认最小行数
        If fieldSize = -1 Then
            fieldSize = series.Size
        Else
            If fieldSize > series.Size Then
                fieldSize = series.Size
            End If
        End If
    Next

    ' 4. 保存列数
    fieldColCount = colArraySize
End Sub

' 读取指定的 sheet 区域内的数据，生成 DataFrame
' 如果没有通过参数 colNames 或 colNameRange 设置列名，将自动使用从 0 开始的索引字符串作为列名
'
' @param rng sheet 区域
' @param colTypes 列类型数组 DF_DataType[]
'                 元素数量必须和参数 rng 中的列数相同
' @optional-param colNames 列名数组
'                          元素数量必须和参数 rng 中的列数相同
' @optional-param colNameRange 包含列名的区域
'                              这个区域内的列数必须和 rng 中的列数相同
'                              如果已经设置了参数 colNames，将会忽略当前参数
Public Sub InitBySheet(ByRef rng As Range, _
    ByRef colTypes As Variant, _
    Optional ByRef colNames As Variant = Empty, _
    Optional ByRef colNameRange As Range)
    ' 1. 检查参数
    If rng Is Nothing Then
        Err.Raise 10001, Description:="DataFrame.InitBySheet: parameter rng must be not nothing"
        Exit Sub
    End If
    If IsArray(colTypes) = False Then
        Err.Raise 10005, Description:="DataFrame.InitBySheet: parameter colTypes must be Array"
        Exit Sub
    End If

    ' colNames 如果不为空，则必须是数组
    If Not IsEmpty(colNames) Then
        If IsArray(colNames) = False Then
            Err.Raise 10005, Description:="DataFrame.InitBySheet: parameter colNames must be Array"
            Exit Sub
        End If
    End If

    ' 2. 保存列数量
    fieldColCount = rng.Columns.Count

    ' 3. 检查列类型数组的数量和列数量是否相同
    Dim colTypeIdxMin As Integer
    Dim colTypeSize As Integer
    colTypeIdxMin = LBound(colTypes)
    colTypeSize = UBound(colTypes) - colTypeIdxMin + 1
    If fieldColCount <> colTypeSize Then
        Err.Raise 10221, Description:="DataFrame.InitBySheet: parameter rng's columns count And array parameter colTypes' size is not same." & vbCrLf & _
                        "rng columns count=" & fieldColCount & ", colTypes's size=" & colTypeSize
        Exit Sub
    End If

    ' 4. 确认列名
    Dim j As Integer
    Dim shCell As Range
    ReDim fieldColumnNames(0 To (fieldColCount - 1))
    Set columnNameIndexMap = UtilsCreateSysObj.createMap

    If Not IsEmpty(colNames) Then
        ' 从列名数组中获取所有的列名
        Dim colNameIdxMin As Integer
        Dim colNamesSize As Integer

        colNameIdxMin = LBound(colNames)
        colNamesSize = UBound(colNames) - colNameIdxMin + 1

        ' 检查各个数组中的元素数量和列数量是否相同
        If fieldColCount <> colNamesSize Then
            Err.Raise 10221, Description:="DataFrame.InitBySheet: parameter rng's columns count And array parameter colNames' size is not same." & vbCrLf & _
                            "rng columns count=" & fieldColCount & ", colNames's size=" & colNamesSize
            Exit Sub
        End If

        ' 保存【列索引列名-->列名的映射】 + 【列名-->列索引的映射】
        For j = 0 To (fieldColCount - 1)
            fieldColumnNames(j) = colNames(colNameIdxMin + j)

            If columnNameIndexMap.Exists(fieldColumnNames(j)) Then
                Err.Raise 10223, Description:="DataFrame.InitBySheet: ColName[" & fieldColumnNames(j) & "] is repeat in parameter colNames(" & (colNameIdxMin + j) & ")"
                Exit Sub
            End If

            columnNameIndexMap.Add fieldColumnNames(j), j
        Next
    ElseIf Not colNameRange Is Nothing Then
        ' 从列名区域中获取所有的列名
        ' 检查各个区域中的单元格数量和列数量是否相同
        If fieldColCount <> colNameRange.Cells.Count Then
            Err.Raise 10221, Description:="DataFrame.InitBySheet: parameter rng's columns count And array parameter colNameRange' cell count is not same." & vbCrLf & _
                            "rng columns count=" & fieldColCount & ", colNameRange' cell count=" & colNameRange.Cells.Count
            Exit Sub
        End If

        ' 保存【列索引列名-->列名的映射】 + 【列名-->列索引的映射】
        j = 0
        For Each shCell In colNameRange
            fieldColumnNames(j) = shCell.Value & ""

            If columnNameIndexMap.Exists(fieldColumnNames(j)) Then
                Err.Raise 10223, Description:="DataFrame.InitBySheet: ColName[" & fieldColumnNames(j) & "] is repeat in parameter colNameRange(" & j & ")"
                Exit Sub
            End If

            columnNameIndexMap.Add fieldColumnNames(j), j

            j = j + 1
        Next
    Else
        ' 如果 colNames + colNameRange 全部都是 Empty，则将 colNames 设置为从 0 开始的索引字符串
        For j = 0 To (fieldColCount - 1)
            fieldColumnNames(j) = j & ""
        Next
    End If

    ' 5. 以列为单位，抽取数据
    fieldSize = rng.Rows.Count

    ReDim fieldColumns(0 To (fieldColCount - 1))
    Dim shColumn As Range
    Dim colSeries As IDFSeries
    Dim realColSeries As DFSeriesInner

    j = 0
    For Each shColumn In rng.Columns
        ' 5.1 初始化一个列并缓存
        Set realColSeries = New DFSeriesInner
        realColSeries.Init dType:= Int(colTypes(j)), size:= fieldSize
        Set colSeries = realColSeries
        Set fieldColumns(j) = colSeries

        ' 5.2 抽取这个列的数据
        For Each shCell In shColumn.Cells
            colSeries.appendData shCell.Value & ""
        Next

        j = j + 1
    Next
End Sub

'################################################################
' Property
'################################################################
' 获取当前 DataFrame 的最大行数
Public Property Get IDataFrame_Size() As Long
    IDataFrame_Size = fieldSize
End Property

' 获取列数
Public Property Get IDataFrame_ColumnCount() As Integer
    IDataFrame_ColumnCount = fieldColCount
End Property

' 获取所有列名
Public Property Get IDataFrame_ColumnNames() As Collection
    Dim result As Collection
    Set result = New Collection

    Dim i As Integer
    For i = 0 To (fieldColCount - 1)
        result.Add fieldColumnNames(i)
    Next

    Set IDataFrame_ColumnNames = result
End Property

' 获取当前 DataFrame 的 Id
Public Property Get IDataFrame_ID() As String
    IDataFrame_ID = fieldId
End Property

' 设置当前 DataFrame 的 Id
Public Property Let IDataFrame_ID(ByRef val As String)
    fieldId = val
End Property

'################################################################
' Function
'################################################################
'--------------------------------------------------
' 行操作
'--------------------------------------------------
' 添加指定数量的空行
' @param count 需要添加的行数，默认为 1 行
Public Sub IDataFrame_appendEmptyRow(Optional count As Long = 1)
    If count <= 0 Then
        Exit Sub
    End If

    ' 1. 遍历所有列对象，全部增加 count 个行
    Dim i As Integer
    For i = 0 To (fieldColCount - 1)
        fieldColumns(i).appendEmpty count
    Next

    ' 2. 增加行数
    fieldSize = fieldSize + count
End Sub

' 新增一行，并拷贝指定行的数据到这个新行
' @param index 需要拷贝的数据行的索引
' @optional-param needDescriptor 如果数据存在描述符，是否需要拷贝，默认=False
Public Sub IDataFrame_copyAndAppendRow(ByRef index As Long, Optional ByRef needDescriptor As Boolean=False)
    ' 1. 检查行索引的范围
    If index < 0 Or index >= fieldSize Then
        Err.Raise 10213, Description:="DataFrame.copyAndAppendRow: parameter index=" & index & " out of bounds. min index=0, max index[" & (fieldSize - 1) & "]"
        Exit Sub
    End If

    ' 2. 遍历所有列对象，全部拷贝指定行
    Dim i As Integer
    For i = 0 To (fieldColCount - 1)
        fieldColumns(i).copyAndAppend index, needDescriptor
    Next

    ' 3. 增加行数
    fieldSize = fieldSize + 1
End Sub

' 直接添加一行数据
' 如果参数数量为 0，则不会执行添加的操作
' 如果参数数量比列的数量少，则剩余的列设置为 Empty
' 如果参数数量比列的数量多，则多余的参数会被忽略
'
' @param colDatas 这一行的每一列的数据
Public Sub IDataFrame_appendRowByColDatas(ParamArray colDatas() As Variant)
    Dim cdArr As Variant
    cdArr = colDatas
    IDataFrame_appendRowByColDataArray cdArr
End Sub

' 直接添加一行数据
' 如果参数数量为 0，则不会执行添加的操作
' 如果参数数量比列的数量少，则剩余的列设置为 Empty
' 如果参数数量比列的数量多，则多余的参数会被忽略
'
' @param colDataArray 这一行的每一列的数据
Public Sub IDataFrame_appendRowByColDataArray(ByRef colDataArray As Variant)
    ' 1. 如果没有有效参数，则立刻停止
    Dim idxMax As Integer
    Dim idxMin As Integer
    Dim idxCount As Integer
    idxMin = LBound(colDataArray)
    idxMax = UBound(colDataArray)
    idxCount = idxMax - idxMin + 1
    If idxCount = 0 Then
        Exit Sub
    End If

    ' 2. 按照参数的数据数量写入数据
    Dim i As Integer

    If idxCount < fieldColCount Then
        ' 2.1. 如果数据数量 < 列数少，则以 idxCount 为最大范围写入列数据
        For i = 0 To (idxCount - 1)
            fieldColumns(i).appendData colDataArray(idxMin + i)
        Next

        '      将剩余的没有数据的列设置为空
        For i = idxCount To (fieldColCount - 1)
            fieldColumns(i).appendEmpty
        Next
    Else
        ' 2.2. 如果数据数量 >= 列数，则以 fieldColCount 为最大范围写入列数据
        For i = 0 To (fieldColCount - 1)
            fieldColumns(i).appendData colDataArray(idxMin + i)
        Next
    End If

    ' 3. 增加行数
    fieldSize = fieldSize + 1
End Sub

' 删除指定索引的行
' 只删除当前对象内的数据
'
' @param index 需要删除的列的索引
'                 如果超出了索引范围，将会抛出异常
Public Sub IDataFrame_delRow(ParamArray indexes() As Variant)
    Dim ies As Variant
    ies = indexes
    IDataFrame_delRowByIndexArray ies
End Sub

' 删除指定索引的行 (使用数组)
' 只删除当前对象内的数据
'
' @param index 需要删除的列的索引
'                 如果超出了索引范围，将会抛出异常
Public Sub IDataFrame_delRowByIndexArray(ByRef indexes As Variant)
    ' 1. 如果没有有效参数，则立刻停止
    Dim idxMax As Integer
    Dim idxMin As Integer
    Dim idxCount As Integer
    idxMin = LBound(indexes)
    idxMax = UBound(indexes)
    idxCount = idxMax - idxMin + 1
    If idxCount = 0 Then
        Exit Sub
    End If

    fieldSize = fieldSize - idxCount

    ' 2. 遍历所有列对象，全部拷贝指定行
    '   不检查行索引范围，由 series 检查
    Dim i As Integer
    For i = 0 To (fieldColCount - 1)
        fieldColumns(i).delDataByIndexArray indexes
    Next

    ' 3. 重新整理描述器缓存
    If Not dataDescriptors Is Nothing Then
        ' 3.1 对 indexes 进行排序
        UtilsSort.sortAscArray indexes

        ' 3.2 开始清除描述器
        Dim startRowIndex As Long
        Dim nextRowIndex As Long
        Dim j As Long
        Dim x As Long

        ' 定位到第一个被删除的行的前一行
        i = indexes(idxMin) - 1
        For j = idxMin + 1 To idxMax + 1
            ' 每次从当前删除行的下一行(有数据的行)开始遍历
            startRowIndex = indexes(j - 1) + 1

            ' 一直遍历到下一个删除行的前一行(有数据的行)
            ' 如果已经超过了最后一个删除索引，则遍历到最后一行
            If j > idxMax Then
                nextRowIndex = fieldColCount - 1
            Else
                nextRowIndex = indexes(j) - 1
            End If

            ' 开始遍历，将所有的描述器向 i 移动
            For x = startRowIndex To nextRowIndex
                i = i + 1

                ' 如果是原地交互则忽略
                If i <> x Then
                    ' 替换前，将旧索引处的描述器删除
                    If dataDescriptors.Exists(i) Then
                        dataDescriptors.Remove i
                    End If

                    ' 然后写入新索引处的描述器
                    If dataDescriptors.Exists(x) Then
                        dataDescriptors.Add i, dataDescriptors.Item(x)
                    End If
                End If
            Next
        Next

        ' 将 i+1 ~ fieldColCount - 1 的所有描述器删除
        ' (这些描述器已经移动到前面的位置了，可以直接删除)
        For j = i + 1 To fieldColCount - 1
            i = i + 1

            If dataDescriptors.Exists(j) Then
                dataDescriptors.Remove j
            End If
        Next
    End If
End Sub

' 删除指定索引的行
' 如果当前是引用对象，也会直接删除原始对象中的数据
'
' @param index 需要删除的列的索引
'                 如果超出了索引范围，将会抛出异常
Public Sub IDataFrame_deepDelRow(ParamArray indexes() As Variant)
    Dim ies As Variant
    ies = indexes
    IDataFrame_delRowByIndexArray ies
End Sub

' 删除指定索引的行 (使用数组)
' 如果当前是引用对象，也会直接删除原始对象中的数据
'
' @param index 需要删除的列的索引
'                 如果超出了索引范围，将会抛出异常
Public Sub IDataFrame_deepDelRowByIndexArray(ByRef indexes As Variant)
    IDataFrame_delRowByIndexArray indexes
End Sub

'--------------------------------------------------
' 列操作
'--------------------------------------------------
' 检查是否存在指定的列
'
' @param cIndex 列的索引或列名
'               如果 index 不是数字(索引) 或 字符串(列名)，则抛出异常
Public Function IDataFrame_hasColumn(ByRef index As Variant) As Boolean
    ' 1. 获取真实的列索引
    If VarType(index) = vbString Then
        ' 如果索引是字符串，则作为列名使用
        If Not columnNameIndexMap.Exists(index) Then
            IDataFrame_hasColumn = False
            Exit Function
        End If
    ElseIf IsNumeric(index) Then
        ' 作为列索引使用
        Dim indexVal As Integer
        indexVal = Int(index)
        If indexVal < 0 Or indexVal >= fieldColCount Then
            IDataFrame_hasColumn = False
            Exit Function
        End If
    Else
        Err.Raise 10012, Description:="DataFrame.hasColumn: please user Integer Or String for parameter"
    End If

    IDataFrame_hasColumn = True
End Function

' 检查是否存在指定的列
' 如果当前是引用对象，也会向下搜索每一层引用对象中的列
'
' @param cIndex 列的索引或列名
'               如果 index 不是数字(索引) 或 字符串(列名)，则抛出异常
Public Function IDataFrame_deepHasColumn(ByRef index As Variant) As Boolean
    IDataFrame_deepHasColumn = IDataFrame_hasColumn(index)
End Function

' 将列索引转换成列名
'
' @param index 列的索引
'              如果超出了索引范围，将会抛出异常
Public Function IDataFrame_colIndexToName(ByRef cIndex As Integer) As String
On Error Goto catch:
    IDataFrame_colIndexToName = fieldColumnNames(cIndex)
    Exit Function
catch:
    ' 处理索引范围异常，其他异常正常抛出
    If Err.Number = 9 Then
        Err.Raise 10210, Description:="DataFrame.colIndexToName: parameter cIndex=" & cIndex & " out of bounds. min index=0, max index[" & (fieldColCount - 1) & "]"
    Else
        Err.Raise Err.Number, Description:=Err.Description
    End If
End Function

' 将列名转换成列索引
'
' @param cName 列名
'              如果列名不存在，将会抛出异常
Public Function IDataFrame_colNameToIndex(ByRef cName As String) As Integer
    If columnNameIndexMap.Exists(cName) Then
        IDataFrame_colNameToIndex = columnNameIndexMap.Item(cName)
    Else
        Err.Raise 10214, Description:="DataFrame.colNameToIndex: can not find column name[" & cName & "]"
    End If
End Function

' 获取列对象
'
' @param index 列的索引（可以使用列索引，也可以使用列名）
'              如果超出了索引范围，将会抛出异常
'              如果列名不存在，将会抛出异常
Public Function IDataFrame_getCol(ByRef index As Variant) As IDFSeries
    ' 1. 获取真实的列索引
    Dim indexVal As Integer
    If VarType(index) = vbString Then
        ' 如果索引是字符串，则作为列名使用
        If Not columnNameIndexMap.Exists(index) Then
            Err.Raise 10214, Description:="DataFrame.getCol: can not find column name[" & index & "]"
        End If

        indexVal = columnNameIndexMap.Item(index)
    ElseIf IsNumeric(index) Then
        ' 作为列索引使用
        indexVal = Int(index)
        If indexVal < 0 Or indexVal >= fieldColCount Then
            Err.Raise 10210, Description:="DataFrame.getCol: parameter cIndex=" & indexVal & " out of bounds. min index=0, max index[" & (fieldColCount - 1) & "]"
            Exit Function
        End If
    Else
        Err.Raise 10012, Description:="please user Integer Or String for parameter"
    End If

    ' 2. 获取列对象
    Set IDataFrame_getCol = fieldColumns(indexVal)
End Function

' 删除指定索引的列
' 只删除当前对象内的数据
'
' @param index 需要删除的列的索引（可以使用列索引，也可以使用列名）
'                 如果超出了索引范围，将会抛出异常
'                 如果列名不存在，将会抛出异常
Public Sub IDataFrame_delCol(ParamArray indexes() As Variant)
    Dim ies As Variant
    ies = indexes
    IDataFrame_delColByIndexArray ies
End Sub

' 删除指定索引的列 (使用数组)
' 只删除当前对象内的数据
'
' @param index 需要删除的列的索引（可以使用列索引，也可以使用列名）
'                 如果超出了索引范围，将会抛出异常
'                 如果列名不存在，将会抛出异常
Public Sub IDataFrame_delColByIndexArray(ByRef indexes As Variant)
    ' 1. 如果没有有效参数，则立刻停止
    Dim idxMax As Integer
    Dim idxMin As Integer
    Dim idxSize As Integer
    idxMin = LBound(indexes)
    idxMax = UBound(indexes)
    idxSize = idxMax - idxMin + 1
    If idxSize = 0 Then
        Exit Sub
    End If

    ' 2. 将所有 indexes 转换成真实的索引
    Dim i As Integer
    Dim indexVal As Variant
    Dim realColIndex() As Integer
    ReDim realColIndex(0 To (idxSize - 1))

    For i = 0 To (idxSize - 1)
        indexVal = indexes(idxMin + i)

        If VarType(indexVal) = vbString Then
            ' 如果索引是字符串，则作为列名使用
            If Not columnNameIndexMap.Exists(indexVal) Then
                Err.Raise 10214, Description:="DataFrame.delColByIndexArray: can not find column name[" & indexVal & "]"
            End If

            realColIndex(i) = columnNameIndexMap.Item(indexVal)
        ElseIf IsNumeric(indexVal) Then
            ' 作为列索引使用
            indexVal = Int(indexVal)
            If indexVal < 0 Or indexVal >= fieldColCount Then
                Err.Raise 10210, Description:="DataFrame.delColByIndexArray: parameter cIndex=" & indexVal & " out of bounds. min index=0, max index[" & (fieldColCount - 1) & "]"
                Exit Sub
            End If

            realColIndex(i) = indexVal
        Else
            Err.Raise 10012, Description:="DataFrame.delColByIndexArray: please user Integer Or String for param[" & (idxMin + i) & "]."
        End If
    Next

    ' 3. 清除所有指定的列
    ' 3.1 将需要删除的列，设置成空槽位（以 "" 为标记）
    For i = 0 To (idxSize - 1)
        fieldColumnNames(realColIndex(i)) = ""
    Next

    ' 3.2 清除空槽位
    Dim j As Integer
    i = -1
    For j = 0 To (fieldColCount - 1)
        If fieldColumnNames(j) <> "" Then
            i = i + 1

            ' 如果是原地修改，则跳过
            If i <> j Then
                ' 删除列索引-->列名映射
                fieldColumnNames(i) = fieldColumnNames(j)
                ' 删除列对象
                Set fieldColumns(i) = fieldColumns(j)
            End If
        End If
    Next

    ' 4. 重新计算列数量
    fieldColCount = fieldColCount - idxSize

    ' 5. 重设列缓存
    ReDim Preserve fieldColumnNames(0 To (fieldColCount - 1))
    ReDim Preserve fieldColumns(0 To (fieldColCount - 1))

    ' 6. 重设: 列名-->列索引的映射
    columnNameIndexMap.RemoveAll
    For j = 0 To (fieldColCount - 1)
        columnNameIndexMap.Add fieldColumnNames(j), j
    Next
End Sub

' 删除指定索引的列
' 如果当前是引用对象，也会直接删除原始对象中的数据
'
' @param index 需要删除的列的索引（可以使用列索引，也可以使用列名）
'                 如果超出了索引范围，将会抛出异常
'                 如果列名不存在，将会抛出异常
Public Sub IDataFrame_deepDelCol(ParamArray indexes() As Variant)
    Dim ies As Variant
    ies = indexes
    IDataFrame_delColByIndexArray ies
End Sub

' 删除指定索引的列 (使用数组)
' 如果当前是引用对象，也会直接删除原始对象中的数据
'
' @param index 需要删除的列的索引（可以使用列索引，也可以使用列名）
'                 如果超出了索引范围，将会抛出异常
'                 如果列名不存在，将会抛出异常
Public Sub IDataFrame_deepDelColByIndexArray(ByRef indexes As Variant)
    IDataFrame_delColByIndexArray indexes
End Sub

' 向所有列的末尾添加一个新的列
' 如果 colName 在 DataFrame 中已经存在，则抛出异常
'
' @param colName 新列的列名
' @param dType 列的类型

Public Sub IDataFrame_addCol(ByRef colName As String, Optional ByRef dType As DF_DataType)
    ' 1. 检查列名是否存在
    If columnNameIndexMap.Exists(colName) Then
        Err.Raise 10217, Description:="DataFrame.addCol: column name[" & colName & "] has been existed"
    End If

    ' 2. 创建新的列对象
    Dim newCol As DFSeriesInner
    Set newCol = New DFSeriesInner
    newCol.Init size:=fieldSize, dType:=dType
    ' 创建到指定长度
    newCol.IDFSeries_appendEmpty fieldSize

    ' 3. 计算新的索引长度，并重建索引关系
    fieldColCount = fieldColCount + 1
    ReDim Preserve fieldColumnNames(0 To (fieldColCount - 1))
    fieldColumnNames((fieldColCount - 1)) = colName
    columnNameIndexMap.Add colName, (fieldColCount - 1)

    ' 4. 保存新的列对象
    ReDim Preserve fieldColumns(0 To (fieldColCount - 1))
    Set fieldColumns((fieldColCount - 1)) = newCol
End Sub
'--------------------------------------------------
' 描述符操作
'--------------------------------------------------

' 获取指定行的描述器
' 如果不存在，则返回 Nothing
'
' @param index 行索引
' @optional-param needCreate 如果描述信息对象不存在是否需要进行创建。默认值=False
' @return IDFDescriptor 数据描述器
Public Function IDataFrame_getRowDescriptor(ByRef index As Long, Optional ByRef needCreate As Boolean = False) As IDFDescriptor
    ' 1. 检查索引范围
    If index < 0 Or index >= fieldSize Then
        Err.Raise 10213, Description:="DataFrame.getRowDescriptor: parameter index=" & index & " out of bounds. min index=0, max index[" & (fieldSize - 1) & "]"
        Exit Function
    End If

    ' 2. 如果描述器缓存还未创建，并且允许创建描述器，则进行创建
    If dataDescriptors Is Nothing Then
        If needCreate = True Then
            ' dataDescriptors 创建缓存
            Set dataDescriptors = UtilsCreateSysObj.createMap
        Else
            ' 如果不允许创建，则立刻结束，并返回 Nothing
            Set IDataFrame_getRowDescriptor = Nothing
            Exit Function
        End If
    End If

    ' 3. 如果缓存中存在，则立刻返回
    If dataDescriptors.Exists(index) = True Then
        Set IDataFrame_getRowDescriptor = dataDescriptors.Item(index)
        Exit Function
    End If

    ' 4. 如果缓存中没有，并且允许创建描述器，则新建
    If needCreate = True Then
        ' 创建描述器并缓存
        Dim result As DFDescriptorInner
        Set result = New DFDescriptorInner
        dataDescriptors.Add index, result

        ' 结束
        Set IDataFrame_getRowDescriptor = result
    Else
        Set IDataFrame_getRowDescriptor = Nothing
    End If
End Function

' 获取指定行列确定的槽位的描述器
' 如果不存在，则返回 Nothing
'
' @param rindex 行索引
' @param cindex 列索引（可以使用列索引，也可以使用列名）
' @optional-param needCreate 如果描述信息对象不存在是否需要进行创建。默认值=False
' @return IDFDescriptor 数据描述器
Public Function IDataFrame_getDataDescriptor(ByRef rindex As Long, ByRef cindex As Variant, Optional ByRef needCreate As Boolean = False) As IDFDescriptor
    ' 1. 获取列对象
    Dim col As IDFSeries
    Set col = IDataFrame_getCol(cindex)

    ' 2. 获取指定行的描述器
    Set IDataFrame_getDataDescriptor = col.getDataDescriptor(rindex, needCreate)
End Function

'--------------------------------------------------
' 数据操作
'--------------------------------------------------
' 读数据
' 使用列索引定位
'
' @param rIndex 行索引
' @param cIndex 列索引
Public Function IDataFrame_getDataByColIndex(ByRef rIndex As Long, ByRef cIndex As Integer) As Variant
On Error Goto catch:
    IDataFrame_getDataByColIndex = fieldColumns(cIndex).getData(rIndex)
    Exit Function
catch:
    ' 处理列索引范围异常，其他异常正常抛出
    If Err.Number = 9 Then
        Err.Raise 10210, Description:="DataFrame.getDataByColIndex: parameter cIndex=" & cIndex & " out of bounds. min index=0, max index[" & (fieldColCount - 1) & "]"
    Else
        Err.Raise Err.Number, Description:=Err.Description
    End If
End Function

' 读数据
' 使用列索引定位
'
' @param rIndex 行索引
' @param cName 列名
Public Function IDataFrame_getDataByColName(ByRef rIndex As Long, ByRef cName As String) As Variant
    ' 1. 检查列名是否存在
    If columnNameIndexMap.Exists(cName) = False Then
        Err.Raise 10215, Description:="DataFrame.getDataByColName: DataFrame do not exist column[" & cName & "]"
        Exit Function
    End If

    ' 2. 读取数据
    IDataFrame_getDataByColName = fieldColumns(columnNameIndexMap.Item(cName)).getData(rIndex)
End Function

' 写数据
' 使用列索引定位
'
' @param rIndex 行索引
' @param cIndex 列索引
Public Sub IDataFrame_setDataByColIndex(ByRef rIndex As Long, ByRef cIndex As Integer, ByRef val As Variant)
On Error Goto catch:
    fieldColumns(cIndex).setData rIndex, val
    Exit Sub
catch:
    ' 处理列索引范围异常，其他异常正常抛出
    If Err.Number = 9 Then
        Err.Raise 10210, Description:="DataFrame.setDataByColIndex: parameter cIndex=" & cIndex & " out of bounds. min index=0, max index[" & (fieldColCount - 1) & "]"
    Else
        Err.Raise Err.Number, Description:=Err.Description
    End If
End Sub

' 写数据
' 使用列索引定位
'
' @param rIndex 行索引
' @param cName 列名
Public Sub IDataFrame_setDataByColName(ByRef rIndex As Long, ByRef cName As String, ByRef val As Variant)
    ' 1. 检查列名是否存在
    If columnNameIndexMap.Exists(cName) = False Then
        Err.Raise 10215, Description:="DataFrame.setDataByColName: DataFrame do not exist column[" & cName & "]"
        Exit Sub
    End If

    ' 2. 读取数据
    fieldColumns(columnNameIndexMap.Item(cName)).setData rIndex, val
End Sub

'--------------------------------------------------
' 检索操作
'--------------------------------------------------
' 创建一个过滤器，以辅助完成检索操作
Public Function IDataFrame_filter() As DFFilter
    Dim result As DFFilter
    Set result = New DFFilter

    result.Init Me

    Set IDataFrame_filter = result
End Function

'--------------------------------------------------
' 切片操作
'--------------------------------------------------
' 切片
' 同时切行 + 列
' 如:
'     df.slice(1,3,1, 2,4,2)
'     df.slice(1,3,1, [1,2,3,4])
'     df.slice(1,3,1, ["a","b","c","d"])
'     df.slice([1,2,3,4], ["a","b","c","d"])
'     df.slice([1,2,3,4], 1,3,1)
'
' @optional-param rowStart 行起始位置
' @optional-param rowStop  行终止位置(最终不会包含 stop 的索引)
' @optional-param rowStep  行起始位置
' @optional-param colStart 列起始位置
' @optional-param colStop 列终止位置(最终不会包含 stop 的索引)
' @optional-param colStep 列起始位置
' @optional-param rowIndexes 行索引数组
'                            如果使用了 rowIndexes, 将会忽略 rowStart + rowStop + rowStep 这三个参数
' @optional-param colIndexes 列索引数组（可以使用数值索引，也可以使用列名字符串）
'                            如果使用了 rowIndexes, 将会忽略 colStart + colStop + colStep 这三个参数
'                            如果使用了列名，但是列名不存在，将会抛出异常
Public Function IDataFrame_slice( _
    Optional ByRef rowStart As Variant, _
    Optional ByRef rowStop As Variant, _
    Optional ByRef rowStep As Variant=1, _
    Optional ByRef colStart As Variant, _
    Optional ByRef colStop As Variant, _
    Optional ByRef colStep As Variant=1, _
    Optional ByRef rowIndexes As Variant, _
    Optional ByRef colIndexes As Variant _
) As IDataFrame
    Dim result As DataFrameSlice
    Set result = New DataFrameSlice
    result.Init Me, _
        rowStart, rowStop, rowStep, _
        colStart, colStop, colStep, _
        rowIndexes, colIndexes

    Set IDataFrame_slice = result
End Function

'--------------------------------------------------
' 输出操作
'--------------------------------------------------
' 将 DataFrame 中的内容输出到控制台
' 按行输出，每个列之间以【,】分割
' Empty 将会以字符串 "@@EMPTY@@" 的形式输出
'
' @optional-param needColName 是否需要输出所有列名。默认=True
Public Sub IDataFrame_printToConsole(Optional ByRef needColName As Boolean = True)

    ' 1. 如果当前 DF 没有任何列，则立刻结束
    If fieldColCount = 0 Then
        Exit Sub
    End If

    ' 2. 输出列名
    Dim line As String
    Dim j As Integer
    If needColName Then
        line = fieldColumnNames(0)
        For j = 1 To (fieldColCount - 1)
            line = line & "," & fieldColumnNames(j)
        Next
        Debug.Print line
    End If

    ' 3. 如果当前 DF 没有任何行，则立刻结束
    If fieldSize = 0 Then
        Exit Sub
    End If

    ' 4. 遍历所有列对象，并输出
    Dim i As Long
    Dim temp As Variant
    For i = 0 To fieldSize - 1
        ' 拼接所有列
        temp = fieldColumns(0).getData(i)
        If IsEmpty(temp) Then
            line = DFHelper.EMPTY_STR_IN_CONSOLE
        Else
            line = temp & ""
        End If

        For j = 1 To (fieldColCount - 1)
            temp = fieldColumns(j).getData(i)

            If IsEmpty(temp) Then
                line = line & "," & DFHelper.EMPTY_STR_IN_CONSOLE
            Else
                line = line & "," & temp
            End If
        Next

        ' 输出这一行
        Debug.Print line
    Next
End Sub


' 将 DataFrame 中的内容输出到指定 sheet 中
' 为了保证输出和现实结果相同，所有单元格将会被设置成字符串，并且所有数据将会被转换成字符串后写出
' 为了加速，默认所有的组件将以列为单位写出数据
'
' @param sh 需要写入数据的 sheet 对象
' @optional-param sheetWriteRowNo 数据写入 sheet 对象时的起始行号.
'                                 默认 = 1
'                                 可用类型，Integer 或者 Long
'
'                                 如果行号小于 0，将会自动使用 1
'                                 如果行号大于最大值 1048576, 将会自动使用 1
'                                 从指定的 sheet 行号开始，最多从 DataFrame 中读取并输出 (1048576 - 行号（如果不输出列名，则 + 1）) 行，多余的行将被省略
'
' @optional-param sheetWriteColNo 数据写入 sheet 对象时的起始列号、或者列名.
'                                 默认 = 1
'                                 如果是列号，请使用 Integer 或者 Long
'                                 如果是列名，请使用字符串
'
'                                 如果列号小于 0，将会自动使用 1
'                                 如果列号大于最大值 16384, 将会自动使用 1
'                                 从指定的 sheet 列号开始，最多从 DataFrame 中读取并输出 (16384 - 行号 + 1) 列，多余的列将被省略
' @optional-param needColName 是否需要在第一行输出所有列名。默认=True
' @optional-param rowDescriptorHandler 行描述器的处理方法名
'                                      应该提供一个形如以下形式的 Module 级别方法，来处理每个行描述器
'                                       Sub rowDescriptorMethod(ByRef ctx As DFSheetWriterContext, ByRef descriptor As IDFDescriptor)
'                                           参数 ctx 表示当前正在使用的上下文对象
'                                           参数 descriptor 存在 Nothing 的情况，需要在方法内手动处理
' @optional-param dataDescriptorHandler 每个数据槽位的描述器的处理方法名
'                                      应该提供一个形如以下形式的 Module 级别方法，来处理每个数据槽位的描述器
'                                       Sub rowDescriptorMethod(ByRef ctx As DFSheetWriterContext, ByRef descriptor As IDFDescriptor)
'                                           参数 ctx 表示当前正在使用的上下文对象
'                                           参数 descriptor 存在 Nothing 的情况，需要在方法内手动处理
' @optional-param colFormatMap 各列的 Format 字符串集合
'                              Map<ColIndex/"ColName", "Format String">
'                              如果没有设置当前参数，所有数据将会直接输出
Public Sub IDataFrame_printToWorksheet(ByRef sh As Worksheet, _
    Optional ByRef sheetWriteRowNo As Variant = -1, _
    Optional ByRef sheetWriteColNo As Variant = -1, _
    Optional ByRef needColName As Boolean = True, _
    Optional ByRef dfRowStart As Long = -1, _
    Optional ByRef dfRowEnd As Long = -1, _
    Optional ByRef rowDescriptorHandler As String = "", _
    Optional ByRef dataDescriptorHandler As String = "", _
    Optional ByRef colFormatMap As Object _
    )

    Dim wc As DFSheetWriterContext
    Set wc = New DFSheetWriterContext

    wc.Init sh, Me, _
        sheetWriteRowNo, _
        sheetWriteColNo, _
        needColName, _
        dfRowStart, _
        dfRowEnd, _
        rowDescriptorHandler, _
        dataDescriptorHandler, _
        colFormatMap
    wc.writeDF
End Sub